/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans.factory.annotation;

import java.lang.reflect.Method;
import java.util.function.Predicate;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Convenience methods performing bean lookups related to annotations, for example
 * Spring's {@link Qualifier @Qualifier} annotation.
 *
 * @author Juergen Hoeller
 * @author Chris Beams
 * @see BeanFactoryUtils
 * @since 3.1.2
 */
public abstract class BeanFactoryAnnotationUtils {

    /**
     * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a
     * qualifier (e.g. via {@code <qualifier>} or {@code @Qualifier}) matching the given
     * qualifier, or having a bean name matching the given qualifier.
     *
     * @param beanFactory the BeanFactory to get the target bean from
     * @param beanType    the type of bean to retrieve
     * @param qualifier   the qualifier for selecting between multiple bean matches
     * @return the matching bean of type {@code T} (never {@code null})
     * @throws NoUniqueBeanDefinitionException if multiple matching beans of type {@code T} found
     * @throws NoSuchBeanDefinitionException   if no matching bean of type {@code T} found
     * @throws BeansException                  if the bean could not be created
     * @see BeanFactory#getBean(Class)
     */
    public static <T> T qualifiedBeanOfType(BeanFactory beanFactory, Class<T> beanType, String qualifier)
            throws BeansException {

        Assert.notNull(beanFactory, "BeanFactory must not be null");

        if (beanFactory instanceof ConfigurableListableBeanFactory) {
            // Full qualifier matching supported.
            return qualifiedBeanOfType((ConfigurableListableBeanFactory) beanFactory, beanType, qualifier);
        } else if (beanFactory.containsBean(qualifier)) {
            // Fallback: target bean at least found by bean name.
            return beanFactory.getBean(qualifier, beanType);
        } else {
            throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() +
                    " bean found for bean name '" + qualifier +
                    "'! (Note: Qualifier matching not supported because given " +
                    "BeanFactory does not implement ConfigurableListableBeanFactory.)");
        }
    }

    /**
     * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier
     * (e.g. {@code <qualifier>} or {@code @Qualifier}) matching the given qualifier).
     *
     * @param bf        the BeanFactory to get the target bean from
     * @param beanType  the type of bean to retrieve
     * @param qualifier the qualifier for selecting between multiple bean matches
     * @return the matching bean of type {@code T} (never {@code null})
     */
    private static <T> T qualifiedBeanOfType(ConfigurableListableBeanFactory bf, Class<T> beanType, String qualifier) {
        String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType);
        String matchingBean = null;
        for (String beanName : candidateBeans) {
            if (isQualifierMatch(qualifier::equals, beanName, bf)) {
                if (matchingBean != null) {
                    throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName);
                }
                matchingBean = beanName;
            }
        }
        if (matchingBean != null) {
            return bf.getBean(matchingBean, beanType);
        } else if (bf.containsBean(qualifier)) {
            // Fallback: target bean at least found by bean name - probably a manually registered singleton.
            return bf.getBean(qualifier, beanType);
        } else {
            throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() +
                    " bean found for qualifier '" + qualifier + "' - neither qualifier match nor bean name match!");
        }
    }

    /**
     * Check whether the named bean declares a qualifier of the given name.
     *
     * @param qualifier   the qualifier to match
     * @param beanName    the name of the candidate bean
     * @param beanFactory the {@code BeanFactory} from which to retrieve the named bean
     * @return {@code true} if either the bean definition (in the XML case)
     * or the bean's factory method (in the {@code @Bean} case) defines a matching
     * qualifier value (through {@code <qualifier>} or {@code @Qualifier})
     * @since 5.0
     */
    public static boolean isQualifierMatch(Predicate<String> qualifier, String beanName,
                                           @Nullable BeanFactory beanFactory) {

        // Try quick bean name or alias match first...
        if (qualifier.test(beanName)) {
            return true;
        }
        if (beanFactory != null) {
            for (String alias : beanFactory.getAliases(beanName)) {
                if (qualifier.test(alias)) {
                    return true;
                }
            }
            try {
                if (beanFactory instanceof ConfigurableBeanFactory) {
                    BeanDefinition bd = ((ConfigurableBeanFactory) beanFactory).getMergedBeanDefinition(beanName);
                    // Explicit qualifier metadata on bean definition? (typically in XML definition)
                    if (bd instanceof AbstractBeanDefinition) {
                        AbstractBeanDefinition abd = (AbstractBeanDefinition) bd;
                        AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName());
                        if (candidate != null) {
                            Object value = candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY);
                            if (value != null && qualifier.test(value.toString())) {
                                return true;
                            }
                        }
                    }
                    // Corresponding qualifier on factory method? (typically in configuration class)
                    if (bd instanceof RootBeanDefinition) {
                        Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod();
                        if (factoryMethod != null) {
                            Qualifier targetAnnotation = AnnotationUtils.getAnnotation(factoryMethod, Qualifier.class);
                            if (targetAnnotation != null) {
                                return qualifier.test(targetAnnotation.value());
                            }
                        }
                    }
                }
                // Corresponding qualifier on bean implementation class? (for custom user types)
                Class<?> beanType = beanFactory.getType(beanName);
                if (beanType != null) {
                    Qualifier targetAnnotation = AnnotationUtils.getAnnotation(beanType, Qualifier.class);
                    if (targetAnnotation != null) {
                        return qualifier.test(targetAnnotation.value());
                    }
                }
            } catch (NoSuchBeanDefinitionException ex) {
                // Ignore - can't compare qualifiers for a manually registered singleton object
            }
        }
        return false;
    }

}
